using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;

namespace CodeStream.Testing.WebAutomation
{
    /// <summary>
    /// Helper class that parses the UI Definition XML and creates the page, class, and field objects
    /// </summary>
    internal static class UIDefinitionFactory
    {
        private static readonly XmlSchema s_schema = InitializeSchema();
        private static readonly XmlNamespaceManager s_nsmgr = InitializeNamespaceManager();

        /// <summary>
        /// Initializes the schema by loading it from the resource.
        /// </summary>
        /// <returns>An XML Schema describing the UI Map xml.</returns>
        static XmlSchema InitializeSchema()
        {
            using (Stream schemaStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("CodeStream.Testing.WebAutomation.UIDefinition.xsd"))
                return XmlSchema.Read(schemaStream, null);
        }

        /// <summary>
        /// Initializes the namespace manager.
        /// </summary>
        /// <returns>A namespace manager with the UI map namespace loaded.</returns>
        static XmlNamespaceManager InitializeNamespaceManager()
        {
            XmlNamespaceManager nsmgr = new XmlNamespaceManager(new NameTable());
            nsmgr.AddNamespace("ns", "http://codestreamtech.com/Testing/WebAutomation/UIDefinition/2011/08");
            return nsmgr;
        }

        /// <summary>
        /// Creates the specified UI map page object using the provided XML reader.
        /// </summary>
        /// <remarks>
        /// Validates the incoming XML based on the schema then parses the data.
        /// </remarks>
        /// <param name="UIDefinitionDefinition">The UI map definition.</param>
        /// <returns>A new UIDefinitionPage object.</returns>
        public static UIDefinitionPage Create(XmlReader UIDefinitionDefinition)
        {
            Contract.Requires(UIDefinitionDefinition != null);
            Contract.Ensures(Contract.Result<UIDefinitionPage>() != null);

            List<XmlSchemaException> schemaErrors = new List<XmlSchemaException>();
            Action<object, ValidationEventArgs> errorHandler = (sender, e) =>
            {
                schemaErrors.Add(e.Exception);
            };

            XmlReaderSettings settings = new XmlReaderSettings()
            {
                CloseInput = false,
                ValidationType = ValidationType.Schema,
                ValidationFlags = XmlSchemaValidationFlags.ProcessIdentityConstraints | XmlSchemaValidationFlags.AllowXmlAttributes | XmlSchemaValidationFlags.ReportValidationWarnings,
            };
            settings.Schemas.Add(s_schema);
            settings.ValidationEventHandler += new ValidationEventHandler(errorHandler);

            XPathNavigator documentNavigator = null;
            using (XmlReader validatingReader = XmlReader.Create(UIDefinitionDefinition, settings))
                documentNavigator = new XPathDocument(validatingReader).CreateNavigator();
            Contract.Assume(documentNavigator != null);

            if (schemaErrors.Count != 0)
                throw new AggregateException("The definition does not conform to the schema", schemaErrors);

            return UIDefinitionFactory.ParseUIDefinitionPage(documentNavigator);
        }

        /// <summary>
        /// Parses the page object.
        /// </summary>
        /// <param name="navigator">The XPathNavigator containing the definition.</param>
        /// <returns>A new UIDefinitionPage object.</returns>
        private static UIDefinitionPage ParseUIDefinitionPage(XPathNavigator navigator)
        {
            Contract.Requires(navigator != null);
            Contract.Ensures(Contract.Result<UIDefinitionPage>() != null);

            XPathNavigator pageNode = navigator.SelectSingleNode("ns:Page", s_nsmgr);
            Contract.Assume(pageNode != null); // verified by schema validation

            var name = pageNode.GetAttribute("name", string.Empty);
            Contract.Assume(!string.IsNullOrEmpty(name)); // verified by schema validation

            var containers =
                from child in pageNode.Select("//ns:Control[*]", s_nsmgr).Cast<XPathNavigator>()
                select ParseContainer(child);

            var fields =
                from child in pageNode.Select("ns:Control", s_nsmgr).Cast<XPathNavigator>()
                select ParseField(child);

            return new UIDefinitionPage(name, containers.ToList().AsReadOnly(), fields.ToList().AsReadOnly());
        }

        /// <summary>
        /// Parses the field object.
        /// </summary>
        /// <param name="definition">The XPathNavigator containing the definition.</param>
        /// <returns>A new UIDefinitionField object.</returns>
        private static UIDefinitionField ParseField(XPathNavigator definition)
        {
            Contract.Requires(definition != null);
            Contract.Ensures(Contract.Result<UIDefinitionField>() != null);

            string name = definition.GetAttribute("name", string.Empty);
            Contract.Assume(!string.IsNullOrEmpty(name)); // verified by schema validation

            string tag = definition.GetAttribute("tag", string.Empty);
            Contract.Assume(!string.IsNullOrEmpty(tag)); // verified by schema validation

            string id = definition.GetAttribute("id", string.Empty).EmptyToNull();
            string cssClass = definition.GetAttribute("class", string.Empty).EmptyToNull();

            bool multiple = definition.GetAttribute("multiple", string.Empty).ParseBool(false);

            string typeName = null;
            if (definition.HasChildren)
            {
                var parentNodes = definition.Select("ancestor-or-self::ns:Control/@name", s_nsmgr).Cast<XPathNavigator>().Select(nav => nav.Value).ToArray();
                typeName = string.Join("_", parentNodes);
            }

            return new UIDefinitionField(name, typeName, tag, id, cssClass, multiple);
        }

        /// <summary>
        /// Parses the class object.
        /// </summary>
        /// <param name="definition">The XPathNavigator containing the definition.</param>
        /// <returns>A new UIDefinitionClass object.</returns>
        private static UIDefinitionClass ParseContainer(XPathNavigator definition)
        {
            Contract.Requires(definition != null);
            Contract.Ensures(Contract.Result<UIDefinitionClass>() != null);

            var nodes = definition.Select("ancestor-or-self::ns:Control/@name", s_nsmgr);
            var parentNodes = nodes.Cast<XPathNavigator>().Select(nav => nav.Value).ToArray();


            var name = string.Join("_", parentNodes);
            Contract.Assume(!string.IsNullOrEmpty(name)); // verified by schema validation

            var tag = definition.GetAttribute("tag", string.Empty);
            Contract.Assume(!string.IsNullOrEmpty(tag)); // verified by schema validation

            var id = definition.GetAttribute("id", string.Empty).EmptyToNull();
            var cssClass = definition.GetAttribute("class", string.Empty).EmptyToNull();

            var fields =
                from child in definition.Select("ns:Control", s_nsmgr).Cast<XPathNavigator>()
                select ParseField(child);

            return new UIDefinitionClass(name, tag, id, cssClass, fields.ToList().AsReadOnly());
        }

        /// <summary>
        /// Converts any instances of an empty string to null.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>The potentially nullified value.</returns>
        private static string EmptyToNull(this string value)
        {
            return (value == null || value.Length == 0) ? null : value;
        }

        /// <summary>
        /// Parses the bool using a default value if the incoming string was empty or null.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="defaultValue">The default bool value.</param>
        /// <returns>A boolean based on the value or the defaultValue.</returns>
        private static bool ParseBool(this string value, bool defaultValue)
        {            
            return (value == null || value.Length == 0) ? defaultValue : value.ToUpperInvariant() == "TRUE";
        }
    }
}
